A payment gateway is a critical component in any online transaction system. It acts as a bridge between the user, the merchant, and financial institutions by securely processing payment requests, verifying details, and ensuring funds are transferred correctly.
No notifications yet
Make a payment to see notifications
sent to customer and merchant
For example, when a customer purchases a product on an e-commerce platform, the payment gateway handles the steps of capturing payment details, validating them, interacting with the bank or wallet provider, and communicating the result (success or failure) to the application.
In this chapter, we will explore the low-level design of a Payment Gateway.
Let’s start by clarifying the requirements:
Designing a payment gateway involves many moving parts. Before diving into the implementation, it's critical to clarify the scope and constraints of the system we are expected to design.
Candidate: Should the payment gateway support multiple payment methods?
Interviewer: Yes, it should support at least Credit Card, PayPal, and UPI. Additional methods can be added later.
Candidate: Should we support retry logic if a payment fails?
Interviewer: Yes, implement a basic retry mechanism—for example, retrying failed payments up to 3 times.
Candidate: What happens after a payment is processed? Should we notify anyone?
Interviewer: Yes, the system should notify the merchant and customer about transaction status updates.
Candidate: Are refunds or reversals in scope?
Interviewer: No. Just implement the core payment flow from request to processing to response.
Candidate: Do we need to support currency conversions?
Interviewer: No, just support basic multi-currency payments, but assume the currency is provided by the merchant.
After gathering the details, we can summarize the key system requirements.
After the requirements are clear, the next step is to identify the core entities that we will form the foundation of our design.
Core entities are the fundamental building blocks of our system. We identify them by analyzing the functional requirements and highlighting the key nouns and responsibilities that naturally map to object-oriented abstractions such as classes, enums, or interfaces.
Let’s walk through the functional requirements and extract the relevant entities:
A merchant's request to initiate a payment will contain various pieces of information, such as the amount, currency, and customer details. This suggests the need for a PaymentRequest entity to encapsulate all this incoming data into a single object. Correspondingly, the system must provide an immediate result of the processing attempt, leading to a PaymentResponse entity to hold the status and a message.
The different payment methods can be represented by a PaymentMethod enum, ensuring type safety. Since the logic for processing a payment is different for each method, we need a common abstraction. This points to a PaymentProcessor interface (Strategy Pattern), which defines a standard processPayment method. We will then have concrete implementations like CreditCardProcessor, PayPalProcessor, and UPIProcessor, each handling the specifics of its method.
Each payment request should be treated as a unique Transaction. This entity will serve as the central record, holding the original PaymentRequest and tracking its lifecycle. The state of this lifecycle (e.g., INITIATED, SUCCESSFUL, FAILED) can be modeled with a PaymentStatus enum.
To avoid coupling the main service with the creation logic of concrete processors, we can introduce a PaymentProcessorFactory. This factory's sole responsibility will be to instantiate and return the appropriate PaymentProcessor based on the PaymentMethod specified in the request.
This is an event-driven requirement, best solved with the Observer pattern. We'll define a PaymentObserver interface for any component that needs to react to transaction updates. Concrete implementations, such as CustomerNotifier and MerchantNotifier, can then subscribe to receive these updates.
To hide the internal complexity of factories, processors, and observers, we need a single, easy-to-use entry point. A PaymentGatewayService will act as a Facade, providing a clean API for merchants to process payments and abstracting away the underlying orchestration.
This section details the design of each class identified previously, including their specific attributes and methods. We will also illustrate how these classes relate to one another and highlight the key design patterns that underpin our solution.
We can categorize our classes into enums, data-holding classes, and core classes that encapsulate the system's primary logic.
A type-safe enumeration to represent the different payment instruments supported by the gateway.
A type-safe enumeration to represent the distinct states a transaction can be in throughout its lifecycle.
A data transfer object (DTO) that encapsulates all the necessary information from a merchant to initiate a payment. It is constructed using the Builder pattern for flexibility and readability.
A simple DTO that carries the immediate synchronous result of a payment processing attempt back to the caller.
The central domain object representing a single payment from start to finish. It links the initial request with its evolving status, serving as a single source of truth for that payment's history.
Acts as the system's Facade and Singleton entry point. It orchestrates the entire payment flow: receiving a request, using the factory to get a processor, executing the payment, updating the transaction status, and notifying all registered observers.
The PaymentProcessor interface and its concrete implementations (CreditCardProcessor, PayPalProcessor, etc.) embody this pattern. Each processor is a different "strategy" for handling a payment. This allows the system to easily support new payment methods by simply adding a new processor class.
The PaymentObserver interface, concrete observers (CustomerNotifier, MerchantNotifier), and the PaymentGatewayService (as the subject) form a classic Observer pattern. This allows different parts of the system to react to transaction status changes without being tightly coupled to the payment processing logic.
The PaymentProcessorFactory centralizes the creation logic for PaymentProcessor objects. This decouples the client (PaymentGatewayService) from the concrete processor classes, making the system more flexible and adhering to the open/closed principle.
The PaymentRequest.Builder provides a clean and fluent API for constructing a PaymentRequest object, which has multiple parameters. This improves readability and is more flexible than using telescoping constructors.
The AbstractPaymentProcessor uses this pattern to define a skeleton algorithm for processing a payment (including retries) while allowing subclasses to override the specific doProcess step. This avoids code duplication (retry logic) across different processors.
The PaymentGatewayService serves as a Facade. It provides a single, simplified interface for merchants to interact with, hiding the complex internal machinery of factories, processors, transactions, and observers.
1class PaymentMethod(Enum):
2 CREDIT_CARD = "CREDIT_CARD"
3 PAYPAL = "PAYPAL"
4 UPI = "UPI"
5
6class PaymentStatus(Enum):
7 INITIATED = "INITIATED"
8 SUCCESSFUL = "SUCCESSFUL"
9 FAILED = "FAILED"1class PaymentRequest:
2 def __init__(self, builder):
3 self.transaction_id = str(uuid.uuid4())
4 self.payer_id = builder.payer_id
5 self.amount = builder.amount
6 self.currency = builder.currency
7 self.payment_method = builder.payment_method
8 self.payment_details = builder.payment_details
9
10 def get_transaction_id(self) -> str:
11 return self.transaction_id
12
13 def get_amount(self) -> float:
14 return self.amount
15
16 def get_currency(self) -> str:
17 return self.currency
18
19 def get_payment_method(self) -> PaymentMethod:
20 return self.payment_method
21
22 class Builder:
23 def __init__(self):
24 self.payer_id = None
25 self.amount = None
26 self.currency = None
27 self.payment_method = None
28 self.payment_details = None
29
30 def payer_id(self, payer_id: str):
31 self.payer_id = payer_id
32 return self
33
34 def amount(self, amount: float):
35 self.amount = amount
36 return self
37
38 def currency(self, currency: str):
39 self.currency = currency
40 return self
41
42 def payment_method(self, payment_method: PaymentMethod):
43 self.payment_method = payment_method
44 return self
45
46 def payment_details(self, payment_details: Dict[str, str]):
47 self.payment_details = payment_details
48 return self
49
50 def build(self) -> 'PaymentRequest':
51 return PaymentRequest(self)1class PaymentResponse:
2 def __init__(self, status: PaymentStatus, message: str):
3 self.status = status
4 self.message = message
5
6 def get_status(self) -> PaymentStatus:
7 return self.status
8
9 def get_message(self) -> str:
10 return self.message1class Transaction:
2 def __init__(self, request: PaymentRequest):
3 self.id = request.get_transaction_id()
4 self.request = request
5 self.status = PaymentStatus.INITIATED
6 self.timestamp = datetime.now()
7
8 def set_status(self, status: PaymentStatus) -> None:
9 self.status = status
10
11 def get_id(self) -> str:
12 return self.id
13
14 def get_status(self) -> PaymentStatus:
15 return self.status
16
17 def get_request(self) -> PaymentRequest:
18 return self.request1class PaymentObserver(ABC):
2 @abstractmethod
3 def on_transaction_update(self, transaction: Transaction) -> None:
4 pass
5
6class CustomerNotifier(PaymentObserver):
7 def on_transaction_update(self, transaction: Transaction) -> None:
8 if transaction.get_status() == PaymentStatus.SUCCESSFUL:
9 print("--- CUSTOMER EMAIL ---")
10 print(f"Your payment of {transaction.get_request().get_amount()} was successful. Transaction ID: {transaction.get_id()}")
11 print("----------------------")
12
13class MerchantNotifier(PaymentObserver):
14 def on_transaction_update(self, transaction: Transaction) -> None:
15 print("--- MERCHANT NOTIFICATION ---")
16 print(f"Transaction {transaction.get_id()} status updated to: {transaction.get_status()}")
17 print("-----------------------------")1class PaymentProcessor(ABC):
2 @abstractmethod
3 def process_payment(self, request: PaymentRequest) -> PaymentResponse:
4 pass
5
6class AbstractPaymentProcessor(PaymentProcessor):
7 MAX_RETRIES = 3
8
9 def process_payment(self, request: PaymentRequest) -> PaymentResponse:
10 attempts = 0
11 while attempts < self.MAX_RETRIES:
12 response = self.do_process(request)
13 attempts += 1
14 if response.get_status() != PaymentStatus.FAILED:
15 break
16 return response
17
18 @abstractmethod
19 def do_process(self, request: PaymentRequest) -> PaymentResponse:
20 pass1class CreditCardProcessor(AbstractPaymentProcessor):
2 def do_process(self, request: PaymentRequest) -> PaymentResponse:
3 print(f"Processing credit card payment of amount {request.get_amount()} {request.get_currency()}")
4 # Simulate interaction with Visa/Mastercard network
5 return PaymentResponse(PaymentStatus.SUCCESSFUL, "Credit Card payment successful.")
6
7class PayPalProcessor(AbstractPaymentProcessor):
8 def do_process(self, request: PaymentRequest) -> PaymentResponse:
9 print(f"Redirecting to PayPal for transaction {request.get_transaction_id()}")
10 # Simulate PayPal API interaction
11 return PaymentResponse(PaymentStatus.SUCCESSFUL, "Paypal payment successful.")
12
13class UPIProcessor(AbstractPaymentProcessor):
14 def do_process(self, request: PaymentRequest) -> PaymentResponse:
15 print(f"Processing UPI payment of {request.get_amount()} {request.get_currency()}")
16 return PaymentResponse(PaymentStatus.SUCCESSFUL, "UPI payment successful.")1class PaymentProcessorFactory:
2 @staticmethod
3 def get_processor(method: PaymentMethod) -> PaymentProcessor:
4 if method == PaymentMethod.CREDIT_CARD:
5 return CreditCardProcessor()
6 elif method == PaymentMethod.UPI:
7 return UPIProcessor()
8 elif method == PaymentMethod.PAYPAL:
9 return PayPalProcessor()
10 else:
11 raise ValueError(f"Unsupported payment method: {method}")1class PaymentGatewayService:
2 _instance = None
3 _lock = Lock()
4
5 def __new__(cls):
6 if cls._instance is None:
7 with cls._lock:
8 if cls._instance is None:
9 cls._instance = super().__new__(cls)
10 cls._instance.observers = []
11 return cls._instance
12
13 @classmethod
14 def get_instance(cls):
15 return cls()
16
17 def add_observer(self, observer: PaymentObserver) -> None:
18 self.observers.append(observer)
19
20 def remove_observer(self, observer: PaymentObserver) -> None:
21 if observer in self.observers:
22 self.observers.remove(observer)
23
24 def _notify_observers(self, transaction: Transaction) -> None:
25 for observer in self.observers:
26 observer.on_transaction_update(transaction)
27
28 def process_payment(self, request: PaymentRequest) -> Transaction:
29 transaction = Transaction(request)
30 try:
31 processor = PaymentProcessorFactory.get_processor(request.get_payment_method())
32 response = processor.process_payment(request)
33 transaction.set_status(response.get_status())
34 except Exception as e:
35 print(f"Payment processing failed: {e}")
36 transaction.set_status(PaymentStatus.FAILED)
37
38 self._notify_observers(transaction)
39 return transaction1def main():
2 # 1. Setup the gateway facade
3 payment_gateway = PaymentGatewayService.get_instance()
4
5 # 2. Register observers to be notified of transaction events
6 payment_gateway.add_observer(MerchantNotifier())
7 payment_gateway.add_observer(CustomerNotifier())
8
9 print("----------- SCENARIO 1: Successful Credit Card Payment -----------")
10 # a. Merchant's backend creates a payment request
11 cc_request = (PaymentRequest.Builder()
12 .payer_id("U-123")
13 .amount(150.75)
14 .currency("INR")
15 .payment_method(PaymentMethod.CREDIT_CARD)
16 .payment_details({"cardNumber": "1234..."})
17 .build())
18
19 # b. Merchant's backend sends it to the facade
20 payment_gateway.process_payment(cc_request)
21
22 print("\n----------- SCENARIO 2: Successful PayPal Payment -----------")
23 paypal_request = (PaymentRequest.Builder()
24 .payer_id("U-456")
25 .amount(88.50)
26 .currency("USD")
27 .payment_method(PaymentMethod.PAYPAL)
28 .payment_details({"email": "[email protected]"})
29 .build())
30
31 payment_gateway.process_payment(paypal_request)
32
33if __name__ == "__main__":
34 main()Which entity in a payment gateway design is responsible for encapsulating all details needed to start a payment?
Shouldn't all the processors implementing
AbstractPaymentProcessorbe singletons? I'm asking because they are stateless, and it might not be efficient to create new instances every time.